package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.api.R;
import com.hmdp.dto.RedisData;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.SystemConstants.*;
import static com.hmdp.utils.SystemConstants.CACHE_SHOP_TTL;

@Slf4j
@Component
public class RedisClient {
    private final StringRedisTemplate stringRedisTemplate;

    public RedisClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //简单通用的保存数据的方法
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    //在保存数据的方法中添加过期时间字段，用于解决缓存击穿
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setRedisData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /*解决缓存穿透问题，
    本质是客户端访问不存在数据导致每次请求都会打到数据库，此方法在Redis保存空字符串，避免空数据打到数据库*/
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFunction,Long time,TimeUnit unit) {
        String key = keyPrefix + id;
        //命中返回"",未命中返回null
        String json = stringRedisTemplate.opsForValue().get(key);//未命中则会有""与null两种情况
        //在redis中查询商铺信息,存在，直接返回
        if (StrUtil.isNotEmpty(json)) {
            R jsonObject = JSONUtil.toBean(json, type);
            return jsonObject;
        }
        //未命中
        //是空的则shopString为空字符串，或者是null
        if (json != null) {
            return null;
        }
        //不存在，去数据库查询用户信息
        R dbObject = dbFunction.apply(id);
        if (Objects.isNull(dbObject)) {
            //不存在，则将空字符串保存在Redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //存在，将数据返回保存在Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(dbObject), time, unit);
        //返回用户信息
        return dbObject;
    }

    /*互斥锁，解决缓存击穿问题，保障一致性
    本质是解决热点数据过期时，多线程同时访问数据造成很大的负担问题，
    互斥锁解决办法：获取锁的线程执行缓存重建，其他的线程休眠等待*/
    public <R, ID> R queryWithMutex(String keyPrefix,String lockKeyPrefix, ID id, Class<R> type, Function<ID, R> dbFunction,Long time,TimeUnit unit) {
        String key = keyPrefix + id;
        String lockKey = lockKeyPrefix + id;
        R jsonObject=null;
        try {
            //命中返回"",未命中返回null
            String json = stringRedisTemplate.opsForValue().get(key);//未命中则会有""与null两种情况
            //在redis中查询商铺信息,存在，直接返回
            if (StrUtil.isNotEmpty(json)) {
                jsonObject = JSONUtil.toBean(json, type);
                return jsonObject;
            }
            //未命中
            //是空的则json为空字符串，或者是null
            if (json != null) {
                return null;
            }
            //获取锁，否则休眠等待
            if (!tryLock(lockKey)) {
                Thread.sleep(50);
                return queryWithMutex(keyPrefix,lockKeyPrefix, id,type, dbFunction,time,unit);
            }
            //不存在，去数据库查询用户信息
            jsonObject = dbFunction.apply(id);
            if (Objects.isNull(jsonObject)) {
                //不存在，则将空字符串保存在Redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //存在，将数据返回保存在Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(jsonObject), CACHE_SHOP_TTL, TimeUnit.MINUTES);
//            redisCache.setCacheObject(key,shop,30,TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            unLock(lockKey);
        }
        //返回用户信息
        return jsonObject;
    }
    /*逻辑过期，解决缓存击穿问题，保障用户体验
    在数据字段中封装过期时间值，查询判断过期时间，
    已过期则开辟新线城执行缓存重构，无论是否过期，最后返回数据*/
    //使用次方法注意只针对热点问题，并且需要提前将热点数据加入到数据库！
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public <R,ID>R queryWithLogicalExpire(String keyPrefix,String lockKeyPrefix, ID id, Class<R> type, Function<ID, R> dbFunction,Long time,TimeUnit unit) {
        String key = keyPrefix + id;
        String lockKey = lockKeyPrefix + id;
        //命中返回"",未命中返回null
        String json = stringRedisTemplate.opsForValue().get(key);
        //在redis中查询商铺信息,不存在直接返回空
        if (StrUtil.isEmpty(json)) {
            return null;
        }
        //序列化成RedisData对象,并获取shop
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R jsonObject = JSONUtil.toBean((JSONObject) redisData.getRedisData(), type);
        //不为空，判断是否已经失效
        //未失效
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            return jsonObject;
        }
        //失效,获取锁，开辟新线程去执行查数据库的操作,执行缓存重建
        if (tryLock(lockKey)) {
            //线程池
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                        try {
                            R jsonObject1 = dbFunction.apply(id);
                            setWithLogicalExpire(keyPrefix+id,jsonObject1,time,unit);
                        } catch (Exception e) {
                            e.printStackTrace();
                        } finally {
                            unLock(lockKey);
                        }
                    }
            );
        }
        //返回用户信息
        return jsonObject;
    }

    private boolean tryLock(String key) {
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }


}
